package com.bumptech.glide.load.resource.bitmap;

import com.bumptech.glide.load.data.UriWrapper;
import com.bumptech.glide.load.data.mediastore.MediaStoreUtil;
import ohos.aafwk.ability.DataAbilityHelper;
import ohos.aafwk.ability.DataAbilityRemoteException;
import ohos.app.Context;
import ohos.media.image.PixelMap;
import ohos.media.photokit.metadata.AVMetadataHelper;
import com.bumptech.glide.util.LogUtil;
import org.jetbrains.annotations.NotNull;
import com.bumptech.glide.load.Option;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.request.target.Target;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;

/**
 * Decodes {@link ohos.media.image.PixelMap} from {@link com.bumptech.glide.load.data.UriWrapper}.
 *
 */
public class VideoDecoder implements ResourceDecoder<UriWrapper, PixelMap> {
    private static final String TAG = "VideoDecoder";

    /**
     * A constant indicating we should use whatever frame we consider best, frequently not the first
     * frame.
     */
    public static final long DEFAULT_FRAME = -1;

    /** Matches the behavior of {@link AVMetadataHelper#fetchVideoPixelMapByTime()} (long)}. */    
    static final int DEFAULT_FRAME_OPTION = AVMetadataHelper.QUERY_OPTION_CLOSEST_SYNC;

    /**
     * A long indicating the time position (in microseconds) of the target frame which will be
     * retrieved. {@link ohos.media.photokit.metadata.AVMetadataHelper#fetchVideoPixelMapByTime(long)} (long)} is used to extract
     * the video frame.
     *
     * <p>When retrieving the frame at the given time position, there is no guarantee that the data
     * source has a frame located at the position. When this happens, a frame nearby will be returned.
     * If the long is negative, time position and option will ignored, and any frame that the
     * implementation considers as representative may be returned.
     */
    public static final Option<Long> TARGET_FRAME =
            Option.disk(
                    "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.TargetFrame",
                    DEFAULT_FRAME,
                    new Option.CacheKeyUpdater<Long>() {
                        private final ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);

                        @Override
                        public void update(
                                @NotNull byte[] keyBytes,
                                @NotNull Long value,
                                @NotNull MessageDigest messageDigest) {
                            messageDigest.update(keyBytes);
                            synchronized (buffer) {
                                buffer.position(0);
                                messageDigest.update(buffer.putLong(value).array());
                            }
                        }
                    });

    /**
     * An integer indicating the frame option used to retrieve a target frame.
     *
     * <p>This option will be ignored if {@link #TARGET_FRAME} is not set or is set to {@link
     * #DEFAULT_FRAME}.
     *
     * @see AVMetadataHelper#fetchVideoPixelMapByTime(long, int)
     */
    // Public API.
    @SuppressWarnings("WeakerAccess")
    public static final Option<Integer> FRAME_OPTION =
            Option.disk(
                    "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.FrameOption",
                    /*defaultValue=*/ AVMetadataHelper.QUERY_OPTION_CLOSEST_SYNC,
                    new Option.CacheKeyUpdater<Integer>() {
                        private final ByteBuffer buffer = ByteBuffer.allocate(Integer.SIZE / Byte.SIZE);

                        @Override
                        public void update(
                                @NotNull byte[] keyBytes,
                                @NotNull Integer value,
                                @NotNull MessageDigest messageDigest) {
                            //noinspection ConstantConditions public API, people could have been doing it wrong
                            if (value == null) {
                                return;
                            }
                            messageDigest.update(keyBytes);
                            synchronized (buffer) {
                                buffer.position(0);
                                messageDigest.update(buffer.putInt(value).array());
                            }
                        }
                    });

    private static final MediaMetadataRetrieverFactory DEFAULT_FACTORY =
            new MediaMetadataRetrieverFactory();

    private final MediaMetadataRetrieverInitializer<UriWrapper> initializer;
    private final BitmapPool bitmapPool;
    private final Context context;
    private final MediaMetadataRetrieverFactory factory;

    public VideoDecoder(BitmapPool bitmapPool, Context context) {
        this(bitmapPool, context, new ParcelFileDescriptorInitializer());
    }


    public VideoDecoder(BitmapPool bitmapPool, Context context, MediaMetadataRetrieverInitializer<UriWrapper> initializer) {
        this(bitmapPool, context, initializer, DEFAULT_FACTORY);
    }

    VideoDecoder(
            BitmapPool bitmapPool,
            Context context,
            MediaMetadataRetrieverInitializer<UriWrapper> initializer,
            MediaMetadataRetrieverFactory factory) {
        this.bitmapPool = bitmapPool;
        this.context = context;
        this.initializer = initializer;
        this.factory = factory;
    }

    @Override
    public boolean handles(@NotNull UriWrapper source, @NotNull Options options) {

        return MediaStoreUtil.isMediaStoreVideoUri( source.getUri());
    }

    private boolean isRequestingDefaultFrame(Options options){
        Long specifiedFrame = options.get(VideoDecoder.TARGET_FRAME);
        return specifiedFrame != null && specifiedFrame == VideoDecoder.DEFAULT_FRAME;
    }

    @Override
    public Resource<PixelMap> decode(
            @NotNull UriWrapper resource, int outWidth, int outHeight, @NotNull Options options)
            throws IOException {

        if (MediaStoreUtil.isThumbnailSize(outWidth, outHeight) && isRequestingDefaultFrame(options)) {
           
            long frameTimeMicros = options.get(TARGET_FRAME);
            if (frameTimeMicros < 0 && frameTimeMicros != DEFAULT_FRAME) {
                throw new IllegalArgumentException(
                        "Requested frame must be non-negative, or DEFAULT_FRAME, given: " + frameTimeMicros);
            }
            Integer frameOption = options.get(FRAME_OPTION);
            if (frameOption == null) {
                frameOption = DEFAULT_FRAME_OPTION;
            }
            DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION);
            if (downsampleStrategy == null) {
                downsampleStrategy = DownsampleStrategy.DEFAULT;
            }

            PixelMap result =null;
            AVMetadataHelper avMetadataHelper= factory.build();
            try {
                initializer.initialize(avMetadataHelper, context, resource);
                result =
                        decodeFrame(
                                avMetadataHelper,
                                frameTimeMicros,
                                frameOption,
                                outWidth,
                                outHeight,
                                downsampleStrategy);
            }
            catch (DataAbilityRemoteException e){}
            finally {

                    avMetadataHelper.close();
            }

            return BitmapResource.obtain(result, bitmapPool);
        }
        else{
            return null;
        }
    }

    private static PixelMap decodeFrame(
            AVMetadataHelper avMetadataHelper,
            long frameTimeMicros,
            int frameOption,
            int outWidth,
            int outHeight,
            DownsampleStrategy strategy) {
        PixelMap result = null;
        // Arguably we should handle the case where just width or just height is set to
        // Target.SIZE_ORIGINAL. Up to and including OMR1, MediaMetadataRetriever defaults to setting
        // the dimensions to the display width and height if they aren't specified (ie
        // getScaledFrameAtTime is not used). Given that this is an optimization only if
        // Target.SIZE_ORIGINAL is not used and not using getScaledFrameAtTime ever would match the
        // behavior of Glide in all versions of Android prior to OMR1, it's probably fine for now.
        if (outWidth != Target.SIZE_ORIGINAL
                && outHeight != Target.SIZE_ORIGINAL
                && strategy != DownsampleStrategy.NONE) {
            result =
                    decodeScaledFrame(
                            avMetadataHelper, frameTimeMicros, frameOption, outWidth, outHeight, strategy);
        }

        if (result == null) {
            result = decodeOriginalFrame(avMetadataHelper, frameTimeMicros, frameOption, outWidth, outHeight);
        }

        // Throwing an exception works better in our error logging than returning null. It shouldn't
        // be expensive because video decoders are attempted after image loads. Video errors are often
        // logged by the framework, so we can also use this error to suggest callers look for the
        // appropriate tags in adb.
        if (result == null) {
            throw new VideoDecoderException();
        }

        return result;
    }

    private static PixelMap decodeScaledFrame(
            AVMetadataHelper avMetadataHelper,
            long frameTimeMicros,
            int frameOption,
            int outWidth,
            int outHeight,
            DownsampleStrategy strategy) {
        try {
            int originalWidth =
                    Integer.parseInt(
                            avMetadataHelper.resolveMetadata(
                                    AVMetadataHelper.AV_KEY_VIDEO_WIDTH));
            int originalHeight =
                    Integer.parseInt(
                            avMetadataHelper.resolveMetadata(
                                    AVMetadataHelper.AV_KEY_VIDEO_HEIGHT));
            int orientation =
                    Integer.parseInt(
                            avMetadataHelper.resolveMetadata(
                                    AVMetadataHelper.AV_KEY_VIDEO_ROTATION));

            if (orientation == 90 || orientation == 270) {
                int temp = originalWidth;
                //noinspection SuspiciousNameCombination
                originalWidth = originalHeight;
                originalHeight = temp;
            }

            float scaleFactor =
                    strategy.getScaleFactor(originalWidth, originalHeight, outWidth, outHeight);

            int decodeWidth = Math.round(scaleFactor * originalWidth);
            int decodeHeight = Math.round(scaleFactor * originalHeight);

            return avMetadataHelper.fetchVideoScaledPixelMapByTime(
                    frameTimeMicros, frameOption, decodeWidth, decodeHeight);
        } catch (Throwable t) {
            // This is aggressive, but we'd rather catch errors caused by reading and/or parsing metadata
            // here and fall back to just decoding the frame whenever possible. If the exception is thrown
            // just from decoding the frame, then it will be thrown and exposed to callers by the method
            // below.
            if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                LogUtil.debug(
                        TAG,
                        "Exception trying to decode a scaled frame on oreo+, falling back to a fullsize frame"
                        + t);
            }

            return null;
        }
    }

    private static PixelMap decodeOriginalFrame(
            AVMetadataHelper avMetadataHelper, long frameTimeMicros, int frameOption, int outWidth, int outHeight) {
        return avMetadataHelper.fetchVideoScaledPixelMapByTime(frameTimeMicros, frameOption, outWidth, outHeight);
    }


    static class MediaMetadataRetrieverFactory {
        public AVMetadataHelper build() {
            return new AVMetadataHelper();
        }
    }


    interface MediaMetadataRetrieverInitializer<UriWrapper> {
        void initialize(AVMetadataHelper retriever, Context context, com.bumptech.glide.load.data.UriWrapper data)
            throws FileNotFoundException, DataAbilityRemoteException;
    }


    // Visible for VideoBitmapDecoder.
    static final class ParcelFileDescriptorInitializer
            implements MediaMetadataRetrieverInitializer<UriWrapper> {

        @Override
        public void initialize(AVMetadataHelper avMetadataHelper, Context context, UriWrapper data) {

            try{
                avMetadataHelper.setSource(DataAbilityHelper.creator(context).openFile( data.getUri(), "r"));
            }
            catch (FileNotFoundException| DataAbilityRemoteException e){
                LogUtil.debug(TAG, "Exception in avMetadataHelper setSource" + e);
            }
        }
    }
    private static final class VideoDecoderException extends RuntimeException {

        private static final long serialVersionUID = -2556382523004027815L;

        VideoDecoderException() {
            super(
                    "MediaMetadataRetriever failed to retrieve a frame without throwing, check the adb logs"
                            + " for .*MetadataRetriever.* prior to this exception for details");
        }
    }
}
